/*
 * GAbstractMoveMaker.java
 * Created on 12 sept. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle.movemaking;


import java.util.List;
import java.util.Random;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.utils.lang.GMath;

/**
 * Common base class for all {@link IGMoveMaker} implementations. Contains common utility
 * methods. Implementors must implement the whole move-making process as part of the
 * {@link #computeMoves()} method.
 *
 * @version $Id$
 * @author jjanke
 */
public abstract class GAbstractMoveMaker implements IGMoveMaker
{
  private static final int GAUSS_MULTIPLIER = 1000;

  protected enum MovedayStatus
  {
    /**
     * Further movedays are to come in the same contest element.
     */
    MORE_MOVEDAYS_TO_COME,

    /**
     * Last moveday of the contest element, but the goal account will be used in further
     * upcoming contest rounds.
     */
    LAST_OF_CONTEST_ELEMENT,

    /**
     * This is the last moveday of the season for the current goal account. I.e. the goal
     * account can be savely emptied.
     */
    LAST_OF_SEASON;
  }

  protected enum MatchResultRecommendation
  {
    /**
     * The move-making algorithm should try to win the next match.
     */
    TRY_TO_WIN,

    /**
     * The move-making algorithm should make a move considering that the match can be
     * lost.
     */
    MAY_LOSE,

    /**
     * No particular recommendation, the algorithm should use its default behaviour to
     * determine whether to win or lose the next match.
     */
    NO_RECOMMENDATION_USE_DEFAULT;
  }

  // Base data
  //
  private IGDao           d_dao;
  private GClub           d_club;
  private GContestElement d_element;
  private GMoveday        d_moveday;
  private boolean         d_fLastMoveday;
  private int             d_nNumKOMatchesLeftSameGA;
  private int             d_nNumRRMatchesLeftSameGA;
  private List<GMatch>    d_listMatches;

  // Result data
  //
  private int[]           d_anSetGoals;
  private int             d_iCurrentMatch;

  @Required
  public void setDao( final IGDao dao )
  {
    d_dao = dao;
  }

  public void setEnvironment( final GClub club, final GContestElement element, final GMoveday moveday,
      final boolean fIsLastMoveday )
  {
    d_club = club;
    d_element = element;
    d_moveday = moveday;
    d_fLastMoveday = fIsLastMoveday;
    d_nNumKOMatchesLeftSameGA = element.getContestRound().getRemainingKOMatchesSameGA();
    d_nNumRRMatchesLeftSameGA = element.getContestRound().getRemainingRRMatchesSameGA();
  }

  /**
   * Launches the move-making process and returns the result of the computation.
   */
  @Transactional( readOnly = true )
  public GMMResult execute() throws GException
  {
    long lStartTime = System.currentTimeMillis();

    // Find matches and execute the move-making algorithm if necessary
    //
    d_listMatches = d_dao.findMatches( d_club, d_element, d_moveday );

    if ( !d_listMatches.isEmpty() )
    {
      d_anSetGoals = new int[d_listMatches.size()];
      setup();
      computeMoves();
    }

    return new GMMResult( d_club, d_listMatches, d_anSetGoals, System.currentTimeMillis() - lStartTime );
  }

  /**
   * This method must be implemented by the actual {@link IGMoveMaker} implementation. It
   * is responsible for setting up all the variables and additional data that may be
   * needed for the actual move-making algorithm which is launched just after this method.
   *
   * @throws GException
   */
  protected abstract void setup() throws GException;

  /**
   * This method must be implemented by the actual {@link IGMoveMaker} implementation.
   * Everything inside this method is executed asynchronously and is not allowed to make
   * write operations to persistent objects.
   *
   * @throws GException if there is any problem during the move-making process
   */
  protected abstract void computeMoves() throws GException;

  /**
   * Returns the amount of goals that have been placed for the match with the given index.
   *
   * @param iMatch the index of the match for which to retrieve the placed goals
   */
  protected int getPlacedGoals( final int iMatch )
  {
    if ( d_iCurrentMatch <= iMatch )
      throw new IllegalStateException( "No goals have been placed until now for match with index " + iMatch );

    return d_anSetGoals[iMatch];
  }

  protected final IGDao getDao()
  {
    return d_dao;
  }

  protected final GClub getClub()
  {
    return d_club;
  }

  protected final GContestElement getElement()
  {
    return d_element;
  }

  /**
   * Returns the current status of the contest element. See the description of the
   * enumeration {@link MovedayStatus} for details about the different statuses.
   */
  protected final MovedayStatus getMovedayStatus()
  {
    if ( d_fLastMoveday )
      if ( getNumMatchesLeft() == 0 )
        return MovedayStatus.LAST_OF_SEASON;
      else
        return MovedayStatus.LAST_OF_CONTEST_ELEMENT;

    return MovedayStatus.MORE_MOVEDAYS_TO_COME;
  }

  /**
   * Returns the number of matches that are remaining to be played after the current
   * contest round. For KO rounds, only the matches to be won are counted. For RR rounds
   * all matches are included in the returned value.
   */
  protected final int getNumMatchesLeft()
  {
    return d_nNumKOMatchesLeftSameGA + d_nNumRRMatchesLeftSameGA;
  }

  /**
   * Returns the number of KO matches that are remaining to be played after the current
   * contest round. Only the matches to be won are counted.
   */
  protected final int getNumKOMatchesLeft()
  {
    return d_nNumKOMatchesLeftSameGA;
  }

  /**
   * Returns the number of RR matches that are remaining to be played after the current
   * contest round.
   */
  protected final int getNumRRMatchesLeft()
  {
    return d_nNumRRMatchesLeftSameGA;
  }

  /**
   * Returns the number of home matches that are likely to be played following the current
   * contest round. The result of this method is a projection and may not be 100% accurate
   * as it is difficult to predict how many matches are played at home/on the road in KO
   * rounds and in RR rounds where the number of home/away matches is not equal.
   *
   * This method assumes that about 2/3 of the outstanding KO matches to be won are played
   * on home ice and half of the outstanding RR matches. If the number of outstanding RR
   * matches is not devidable by 2, it is assumed that the team has 1 one home match more
   * than away matches. I.e. this method is slightly optimistic about the home/away match
   * distribution.
   */
  protected final int getNumHomeMatchesLeft()
  {
    int nKO = (int) Math.ceil( d_nNumKOMatchesLeftSameGA * 2 / 3.0 );
    int nRR = (int) Math.ceil( d_nNumRRMatchesLeftSameGA / 2.0 );

    return nKO + nRR;
  }

  /**
   * Returns the number of away matches that are likely to be played following the current
   * contest round. The result of this method is a projection and may not be 100% accurate
   * as it is difficult to predict how many matches are played at home/on the road in KO
   * rounds and in RR rounds where the number of home/away matches is not equal.
   */
  protected final int getNumAwayMatchesLeft()
  {
    return getNumMatchesLeft() - getNumHomeMatchesLeft();
  }

  protected final List<GMatch> getMatches()
  {
    return d_listMatches;
  }

  /**
   * Places the goals for the last simulated match.
   *
   * @param nGoals the goals to be set
   */
  protected final void placeGoals( final int nGoals )
  {
    d_anSetGoals[d_iCurrentMatch++] = nGoals;
  }

  /**
   * Determines the goals to place by using the current state of the probabilities array
   * and a random generator. The determined number of goals is then directly set to the
   * provided match.
   *
   * @param match the match for which to place the goals
   * @param club the club for which to place the goals
   * @param anGoalProbabilities the prepared probabilities array
   * @param nMaxGoalsForMatch the maximum number of goals that is allowed to be set for
   *          the match
   */
  protected final void placeGoals( final GMatch match, final GClub club, final int[] anGoalProbabilities,
      final int nMaxGoalsForMatch )
  {
    // Make the move by using the probability array. First the array needs to be
    // transformed.
    //
    for ( int i = 1; i < anGoalProbabilities.length; i++ )
      anGoalProbabilities[i] += anGoalProbabilities[i - 1];

    // Generate random number between 0 and the value in the last element of the
    // probability array (1 is added because the value is exclusive)
    //
    int nRandomNumber = new Random().nextInt( anGoalProbabilities[anGoalProbabilities.length - 1] + 1 );

    // Check to which number of goals the generated random value corresponds
    //
    int nGoals = -1;
    for ( int i = 0; i < anGoalProbabilities.length; i++ )
      if ( nRandomNumber <= anGoalProbabilities[i] )
      {
        nGoals = i;
        break;
      }

    assert ( nGoals > -1 && nGoals <= nMaxGoalsForMatch ) : "The number of calculated goals is wrong: " + nGoals
        + " match ID " + match.getId();

    // Set goals to match
    //
    placeGoals( nGoals );
  }

  /**
   * Adds values to all elements of the probability array according to the given
   * parameters. The values to add are calculate by using the gauss algorithm.
   *
   * @param anGoalProbabilities the probability array to be manipulated
   * @param dExpected the expected value of the gauss curve
   * @param dStdDev the standard deviation
   * @param dFactor the multiplier to be applied to the calculated gauss value
   */
  protected final void manipulateProbabilitiesGauss( final int[] anGoalProbabilities, final double dExpected,
      final double dStdDev, final double dFactor )
  {
    for ( int i = 0; i < anGoalProbabilities.length; i++ )
      anGoalProbabilities[i] += GAUSS_MULTIPLIER * GMath.gauss( i, dFactor, dStdDev, dExpected );
  }

  /**
   * Returns the maximum possible number of goals, the current team may score for the
   * specified match.
   *
   * @param match the match for which to calculate the max possible number of goals
   * @param club the club for which to obtain the max number of possible goals
   * @param dAvailableGoals the currently available number of goals
   * @return the maximum possible number of goals
   */
  protected final int getNumMaxPossibleGoals( final GMatch match, final GClub club, final double dAvailableGoals )
  {
    final double dOneGoal = match.getCostOfOneGoal( club ).doubleValue();
    final double dMaxCost = dOneGoal * GRulesConstants.MAX_NUM_GOALS_PER_MATCH;

    if ( dMaxCost < dAvailableGoals )
      return GRulesConstants.MAX_NUM_GOALS_PER_MATCH;

    // We round up because it's better to score one goal too much than too few (when
    // matches are analysed, non possible goal placements will be automatically
    // corrected). By using Math.min() we make sure that the maximum number of possible
    // goals is considered.
    //
    return Math.min( (int) Math.ceil( dAvailableGoals / dOneGoal ), GRulesConstants.MAX_NUM_GOALS_PER_MATCH );
  }
}
